Skip to content

a11y plugin #326

Merged
harry-whorlow merged 76 commits intoTanStack:mainfrom
LadyBluenotes:a11y
Mar 13, 2026
Merged

a11y plugin #326
harry-whorlow merged 76 commits intoTanStack:mainfrom
LadyBluenotes:a11y

Conversation

@LadyBluenotes
Copy link
Member

@LadyBluenotes LadyBluenotes commented Jan 21, 2026

🎯 Changes

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features

    • Added TanStack Accessibility (A11y) DevTools plugin: run audits, click-to-navigate, visual overlays, issue highlighting, severity filtering, rule-set selection, enable/disable rules, and export results (JSON/CSV).
    • DevTools panels for React and Solid with example projects and integrated Run Audit / Export controls.
  • Documentation

    • Comprehensive A11y plugin guide with installation, quick-start, usage, configuration, severity mapping, troubleshooting, and examples.
  • Tests

    • Added unit tests for config and export utilities.

@changeset-bot
Copy link

changeset-bot bot commented Jan 21, 2026

🦋 Changeset detected

Latest commit: a20fe4a

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
@tanstack/solid-devtools Patch
@tanstack/devtools-a11y Patch
@tanstack/devtools-ui Patch
@tanstack/devtools Patch
@tanstack/devtools-utils Patch
@tanstack/preact-devtools Patch
@tanstack/react-devtools Patch
@tanstack/vue-devtools Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@nx-cloud
Copy link

nx-cloud bot commented Jan 21, 2026

View your CI Pipeline Execution ↗ for commit a20fe4a

Command Status Duration Result
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 38s View ↗
nx affected --targets=test:eslint,test:sherif,t... ✅ Succeeded 9s View ↗

☁️ Nx Cloud last updated this comment at 2026-03-13 13:33:40 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 21, 2026

More templates

@tanstack/devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools@326

@tanstack/devtools-a11y

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-a11y@326

@tanstack/devtools-client

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-client@326

@tanstack/devtools-ui

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-ui@326

@tanstack/devtools-utils

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-utils@326

@tanstack/devtools-vite

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-vite@326

@tanstack/devtools-event-bus

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-bus@326

@tanstack/devtools-event-client

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/devtools-event-client@326

@tanstack/preact-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/preact-devtools@326

@tanstack/react-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/react-devtools@326

@tanstack/solid-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/solid-devtools@326

@tanstack/vue-devtools

npm i https://pkg.pr.new/TanStack/devtools/@tanstack/vue-devtools@326

commit: 15c61b0

@Stybo
Copy link

Stybo commented Feb 21, 2026

@LadyBluenotes Any expected release date? Really curious

@coderabbitai
Copy link

coderabbitai bot commented Mar 13, 2026

📝 Walkthrough

Walkthrough

Adds a new @tanstack/devtools-a11y package with core auditing, Solid UI components, React/Solid plugins, examples, docs, tests, and devtools registry integration. Implements axe-core + custom audits, overlays/tooltips, export (JSON/CSV), configuration persistence, and theme-syncing changes in devtools UI.

Changes

Cohort / File(s) Summary
A11y package core (types & utils)
packages/devtools-a11y/src/core/types/types.ts, packages/devtools-a11y/src/core/utils/ally-audit.utils.ts, packages/devtools-a11y/src/core/utils/custom-audit.utils.ts, packages/devtools-a11y/src/core/utils/config.utils.ts, packages/devtools-a11y/src/core/utils/export-audit.uitls.ts, packages/devtools-a11y/src/core/utils/ui.utils.ts
New type definitions and heavy logic: axe-core integration, custom rule engines, audit runner, config persistence, export (JSON/CSV), DOM highlighting/tooltip system. These are high-surface-area files—review data shapes, DOM selectors, and error handling.
A11y UI components & context
packages/devtools-a11y/src/core/components/..., packages/devtools-a11y/src/core/contexts/allyContext.tsx, packages/devtools-a11y/src/core/styles/styles.ts
SolidJS UI: Shell, IssueList, IssueCard, Settings overlay, AllyProvider context and theme-aware styles. Inspect reactive flows, selection/highlight side effects, and style tokens.
Framework integrations (React & Solid)
packages/devtools-a11y/src/react/..., packages/devtools-a11y/src/solid/...
Panel and plugin wrappers for React and Solid, environment-conditional exports, production variants and plugin registration modules.
Package scaffolding & build/test
packages/devtools-a11y/package.json, packages/devtools-a11y/tsconfig.json, packages/devtools-a11y/vite.config.ts, packages/devtools-a11y/eslint.config.js, packages/devtools-a11y/tsconfig.json
New package manifest, export map (core/react/solid), TS/Vite configs and ESLint — check export paths and build entries.
A11y tests
packages/devtools-a11y/tests/*
Unit tests for config and export utilities plus test setup. Validate mocks and environment assumptions.
Examples (React & Solid)
examples/react/a11y-devtools/..., examples/solid/a11y-devtools/...
New example projects demonstrating plugin usage and intentional a11y issues. Verify example configs and workspace path aliases.
Docs & site config
docs/plugins/a11y.md, docs/config.json
Comprehensive A11y plugin documentation and new Plugins entry in docs config (note: one path shows a likely typo: framework/solif/...).
Devtools integration & UI tweaks
packages/devtools/src/tabs/plugin-registry.ts, packages/devtools/src/devtools.tsx, packages/devtools/src/styles/use-styles.ts, packages/devtools-ui/src/*, packages/devtools-utils/src/solid/class.ts
Adds plugin registry entry, theme sync to document dataset, minor style/behavior adjustments (button styling refactor, scroll removal, Solid unmount fix). Review theme and style regressions.
Misc examples updates
examples/react/basic/src/setup.tsx, examples/react/start/src/routes/__root.tsx, examples/react/bundling-repro/.wrangler/deploy/config.json
Minor example edits: duplicated plugin entry (possible bug), refactor plugins array, path style normalization.

Sequence Diagram

sequenceDiagram
  participant User as Developer (UI)
  participant App as App / Example
  participant Devtools as TanStack Devtools Plugin
  participant A11yCore as A11y Core (runner)
  participant Axe as axe-core
  participant Storage as localStorage
  participant Export as Export Utils

  User->>App: trigger "Run Audit"
  App->>Devtools: invoke plugin.triggerAllyScan()
  Devtools->>A11yCore: runAudit(options)
  A11yCore->>Axe: execute axe with context & rules
  A11yCore->>A11yCore: run custom rules merge results
  A11yCore-->>Devtools: return A11yAuditResult
  Devtools->>Storage: persist config (if enabled)
  Devtools->>App: render Shell with IssueList and overlays
  User->>Devtools: click "Export"
  Devtools->>Export: exportAuditResults(result, format)
  Export->>User: download file (JSON/CSV)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐰 I hop through code with whiskers bright,

I sniff out issues in the night,
With overlays, rules, and export song,
I help your UI do no wrong—
A11y cheer, hop on and delight! 🥕

🚥 Pre-merge checks | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description follows the template structure but leaves the 'Changes' section empty, providing no details about what was implemented or why. Fill in the 'Changes' section with a clear explanation of the new accessibility plugin features, its integration points, and motivation for the implementation.
Docstring Coverage ⚠️ Warning Docstring coverage is 74.55% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'a11y plugin' is vague and generic, lacking specificity about what the plugin does or its scope within the changeset. Consider a more descriptive title like 'Add a11y DevTools plugin for accessibility auditing' to clarify the purpose and scope of changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/solid-devtools/src/core.tsx (2)

61-105: ⚠️ Potential issue | 🟡 Minor

Update the name JSDoc to match the new public type.

name now accepts SolidPluginRender, so the theme-props callback form is public there too, but the current doc block still says function names are called with the mount element only. Since this JSDoc is published, the generated docs will be misleading until that section is updated as well.

📝 Suggested doc update
   /**
    * Name to be displayed in the devtools UI.
    * If a string, it will be used as the plugin name.
-   * If a function, it will be called with the mount element.
+   * If a function, it can either receive the mount element and current theme,
+   * or a props object containing the current theme.

As per coding guidelines, JSDoc should read like documentation as it gets converted to markdown docs for the website.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/solid-devtools/src/core.tsx` around lines 61 - 105, Update the JSDoc
for the exported property `name` (whose type is now `string |
SolidPluginRender`) to reflect that when a function is provided it receives the
same theme props as `render` (not just the mount element); explicitly document
the callback signature (e.g., receives { theme } or theme props) and show the
example forms (string, JSX element, or a callback that receives theme props) so
the generated docs match the public `SolidPluginRender` type.

17-55: ⚠️ Potential issue | 🟠 Major

Don't dispatch between callback shapes with Function.length.

This is ambiguous with existing one-argument callbacks. render: (el) => ..., name: (el) => ..., and (el, theme = 'light') => ... all report length === 1, so they incorrectly go down the props branch and receive { theme } instead of the mount node. That breaks any existing code using single-argument callbacks. Make the props-style form explicit instead of inferring it from arity.

Additionally, the name JSDoc (lines 84–104) is incomplete. It does not document that name can be a function accepting either (el, theme) or { theme } props, creating a mismatch between the documented and actual API surface. Update the documentation to be consistent with the render JSDoc.

🟡 Minor comments (8)
docs/config.json-181-183 (1)

181-183: ⚠️ Potential issue | 🟡 Minor

Fix broken docs path for Solid plugin example.

Line 182 uses framework/solif/..., which looks like a typo and will produce a broken link.

Fix
-              "to": "framework/solif/examples/a11y-devtools"
+              "to": "framework/solid/examples/a11y-devtools"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/config.json` around lines 181 - 183, The "to" path for the A11y example
is misspelled as "framework/solif/examples/a11y-devtools" causing a broken docs
link; update the "to" value (the JSON entry paired with "label": "A11y") to the
correct path "framework/solid/examples/a11y-devtools" so the link resolves to
the Solid plugin example.
docs/plugins/a11y.md-10-10 (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Use “Full-Page Scanning” for compound adjective consistency.
Small docs polish: hyphenate “Full Page” when used adjectivally.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plugins/a11y.md` at line 10, Replace the adjectival phrase "Full Page
Scanning" with the hyphenated "Full-Page Scanning" in the docs text (the
occurrence in docs/plugins/a11y.md), ensuring the compound adjective is
consistently hyphenated wherever the phrase is used as a descriptor.
packages/devtools-a11y/tests/config.test.ts-27-34 (1)

27-34: ⚠️ Potential issue | 🟡 Minor

Add vi.unstubAllGlobals() in the afterEach hook to properly clean up global stubs.

The config does not enable unstubGlobals: true, so vi.restoreAllMocks() alone will not undo the vi.stubGlobal('localStorage') call. Without cleanup, the mocked localStorage will leak into later test suites. Either add vi.unstubAllGlobals() here or enable unstubGlobals: true in the Vitest config.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/tests/config.test.ts` around lines 27 - 34, The
afterEach cleanup currently calls vi.restoreAllMocks() but does not undo globals
stubbed by vi.stubGlobal('localStorage') in beforeEach; update the afterEach
hook to also call vi.unstubAllGlobals() (in addition to vi.restoreAllMocks()) to
ensure the stubbed global localStorage from localStorageMock is removed between
tests, referencing the existing beforeEach/afterEach and the
vi.stubGlobal('localStorage') usage so the global cleanup is applied correctly.
examples/solid/a11y-devtools/.eslintrc.cjs-4-9 (1)

4-9: ⚠️ Potential issue | 🟡 Minor

Move extends and rules to the ESLint config top level.

In legacy .eslintrc.* format, extends and rules are top-level config properties. The settings key is reserved for shared plugin data only. Lines 6–7 place them incorrectly under settings, where ESLint does not read them.

Proposed fix
 const config = {
-  settings: {
-    extends: [],
-    rules: {},
-  },
+  extends: [],
+  rules: {},
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/solid/a11y-devtools/.eslintrc.cjs` around lines 4 - 9, The ESLint
config puts extends and rules under the reserved settings key so ESLint ignores
them; update the exported config object named config by moving extends and rules
out of config.settings into the top-level config (keep config.settings only for
plugin shared data or remove it if unused), e.g., ensure config.extends = [...]
and config.rules = {...} and delete these keys from config.settings so ESLint
reads them correctly.
examples/react/a11y-devtools/src/App.tsx-66-68 (1)

66-68: ⚠️ Potential issue | 🟡 Minor

Mismatched comment and heading.

The JSX comment on line 66 says "Missing main landmark" but the corresponding heading says "Click handler on non-interactive element". Update the comment to match the demonstrated issue.

✏️ Proposed fix
-        {/* Issue: Missing main landmark */}
+        {/* Issue: Click handler on non-interactive element */}
         <div style={{ marginBottom: '16px' }}>
           <h3>6. Click handler on non-interactive element</h3>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/a11y-devtools/src/App.tsx` around lines 66 - 68, Update the
JSX comment above the example to match the displayed issue: change the comment
text in the div block that contains the h3 ("6. Click handler on non-interactive
element") so the comment reads "Click handler on non-interactive element"
instead of "Missing main landmark"; locate the comment in the same JSX fragment
containing the div and h3 in App (App.tsx) and replace the mismatched comment
text accordingly.
examples/solid/a11y-devtools/src/app.tsx-63-65 (1)

63-65: ⚠️ Potential issue | 🟡 Minor

Mismatched comment and heading.

Same issue as in the React example: the JSX comment says "Missing main landmark" but the heading says "Click handler on non-interactive element".

✏️ Proposed fix
-        {/* Issue: Missing main landmark */}
+        {/* Issue: Click handler on non-interactive element */}
         <div style="margin-bottom: 16px;">
           <h3>6. Click handler on non-interactive element</h3>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/solid/a11y-devtools/src/app.tsx` around lines 63 - 65, The inline
JSX comment above the example div is inconsistent with its heading: update
either the JSX comment or the <h3> text so they match; locate the div with
style="margin-bottom: 16px" that contains the <h3> and change the comment to
"Click handler on non-interactive element" to match the heading (or
alternatively change the <h3> to "Missing main landmark" if that was intended).
packages/devtools-a11y/src/core/utils/ally-audit.utils.ts-110-123 (1)

110-123: ⚠️ Potential issue | 🟡 Minor

Potential ID collision in rapid iteration.

Using Date.now() for ID generation within a synchronous loop can produce duplicate IDs when multiple nodes are processed within the same millisecond. Additionally, Date.now() is called twice per issue (once for id, once for timestamp).

🔧 Suggested fix
+let issueCounter = 0
+
 function convertToIssues(
   results: AxeResults,
   threshold: SeverityThreshold,
 ): Array<A11yIssue> {
   const issues: Array<A11yIssue> = []
+  const batchTimestamp = Date.now()

   for (const violation of results.violations) {
     const impact = violation.impact as SeverityThreshold | undefined

     for (let i = 0; i < violation.nodes.length; i++) {
       const node = violation.nodes[i]!
       const selector = node.target.join(', ')

       const a11yNode: A11yNode = {
         selector,
         html: node.html,
         xpath: node.xpath?.join(' > '),
         failureSummary: node.failureSummary,
       }

       issues.push({
-        id: `${violation.id}-${i}-${Date.now()}`,
+        id: `${violation.id}-${i}-${batchTimestamp}-${issueCounter++}`,
         ruleId: violation.id,
         impact: impact || 'minor',
         message: node.failureSummary || violation.description,
         help: violation.help,
         helpUrl: violation.helpUrl,
         wcagTags: violation.tags.filter(
           (tag) => tag.startsWith('wcag') || tag.startsWith('section508'),
         ),
         nodes: [a11yNode],
         meetsThreshold: meetsThreshold(impact, threshold),
-        timestamp: Date.now(),
+        timestamp: batchTimestamp,
       })
     }
   }

   return issues
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/ally-audit.utils.ts` around lines 110 -
123, The issue: IDs use Date.now() inside the synchronous loop (and called
twice) which can collide; fix by generating a stable timestamp and a truly
unique id per issue. Before pushing, compute a single timestamp variable (e.g.,
const timestamp = Date.now()) and reuse it for the timestamp field, and replace
the id generation with a collision-resistant value (e.g., use
crypto.randomUUID() or append a short random/nonce to
`${violation.id}-${i}-${timestamp}`) so each issue created in the same
millisecond is unique; keep other fields (ruleId, message, help, wcagTags,
nodes, meetsThreshold) unchanged.
packages/devtools-a11y/src/core/utils/ally-audit.utils.ts-240-245 (1)

240-245: ⚠️ Potential issue | 🟡 Minor

Incorrect type assertion on include array element.

When context is a Document, casting it as Element at line 242 violates type safety. The include array should accept Node (which covers both Document and Element), not just Element. Change the cast to as Node or use as any if Node is not available, since axe-core's ElementContext accepts Node rather than the narrower Element type.

    if (allExclusions.length > 0) {
      auditContext = {
        include: [auditContext as Node],
        exclude: allExclusions.map((sel) => [sel]),
      } as axe.ElementContext
    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/ally-audit.utils.ts` around lines 240 -
245, The current code casts auditContext to Element when building the
axe.ElementContext (in the if block checking allExclusions.length) which is
unsafe if auditContext can be a Document; change the include array element cast
from Element to Node (e.g., use auditContext as Node) so the type matches
axe-core's ElementContext, and keep the exclude mapping as-is
(allExclusions.map((sel) => [sel])) while preserving the final as
axe.ElementContext assertion.
🧹 Nitpick comments (19)
examples/react/start/src/routes/__root.tsx (2)

1-1: Use a type-only import for ReactNode on Line 1.

Since React is only used for the React.ReactNode type annotation and the project uses the modern jsx: "react-jsx" transform, import ReactNode as a type-only import.

♻️ Proposed fix
-import * as React from 'react'
+import type { ReactNode } from 'react'
@@
-function RootDocument({ children }: { children: React.ReactNode }) {
+function RootDocument({ children }: { children: ReactNode }) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start/src/routes/__root.tsx` at line 1, Replace the namespace
import "import * as React from 'react'" with a type-only import and update
usages: change to "import type { ReactNode } from 'react'" and replace any
"React.ReactNode" annotations with "ReactNode" (e.g., in the return/props types
or component signatures); this keeps the runtime bundle free of an unused React
import while preserving the type annotation.

39-50: Hoist the plugins array to module scope to avoid unnecessary recalculations.

The plugins array is recreated on every RootDocument render. Since TanStackDevtools includes this in a useMemo dependency array (packages/react-devtools/src/devtools.tsx:172), each new reference triggers recalculation and a subsequent useEffect that calls devtools.setConfig(). Moving plugins to module scope stabilizes the reference and eliminates this overhead.

♻️ Proposed fix
+const DEVTOOLS_PLUGINS = [
+  {
+    name: 'Tanstack Router',
+    render: <TanStackRouterDevtoolsPanel />,
+  },
+  {
+    id: 'route-navigation',
+    name: 'Route Navigation',
+    render: <RouteNavigationPanel />,
+  },
+]
+
 function RootDocument({ children }: { children: ReactNode }) {
   console.log('Rendering Root Document')
-  const plugins = [
-    {
-      name: 'Tanstack Router',
-      render: <TanStackRouterDevtoolsPanel />,
-    },
-    {
-      id: 'route-navigation',
-      name: 'Route Navigation',
-      render: <RouteNavigationPanel />,
-    },
-  ]
@@
-          plugins={plugins}
+          plugins={DEVTOOLS_PLUGINS}
         />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/start/src/routes/__root.tsx` around lines 39 - 50, The plugins
array is being recreated on every RootDocument render causing unstable
references for TanStackDevtools (which uses it in a useMemo dependency) and
triggering unnecessary devtools.setConfig() calls; fix this by hoisting the
plugins declaration to module scope (move the const plugins = [...] out of the
RootDocument component) so the same array reference is reused, and keep the
existing items (TanStackRouterDevtoolsPanel, RouteNavigationPanel) intact when
referenced by TanStackDevtools/Devtools.
packages/devtools/src/devtools.tsx (1)

194-200: Consider restoring previous root dataset value on cleanup.

This currently leaves a global data-tanstack-devtools-theme side effect after unmount. Cleanup would make it safer for embed/test scenarios.

Possible refactor
   createEffect(() => {
     if (typeof document === 'undefined') {
       return
     }
-
-    document.documentElement.dataset.tanstackDevtoolsTheme = theme()
+    const root = document.documentElement
+    const prev = root.dataset.tanstackDevtoolsTheme
+    root.dataset.tanstackDevtoolsTheme = theme()
+    onCleanup(() => {
+      if (prev === undefined) {
+        delete root.dataset.tanstackDevtoolsTheme
+      } else {
+        root.dataset.tanstackDevtoolsTheme = prev
+      }
+    })
   })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools/src/devtools.tsx` around lines 194 - 200, The effect
currently sets document.documentElement.dataset.tanstackDevtoolsTheme but never
restores it on unmount; capture the previous value (e.g. const prev =
document.documentElement.dataset.tanstackDevtoolsTheme) before assigning
theme(), then register a cleanup to restore it (if prev is undefined delete the
dataset key, otherwise set it back) using Solid's onCleanup inside the
createEffect; update the createEffect block that uses theme() and
document.documentElement.dataset.tanstackDevtoolsTheme to perform this
save-assign-restore sequence.
packages/devtools-a11y/tests/index.test.ts (1)

3-6: Replace tautological test with a real smoke assertion.

Line 5 always passes and won’t catch regressions. Please assert an actual export/runtime behavior for the package entrypoint.

Suggested direction
 describe('devtools', () => {
-  it('should pass', () => {
-    expect(true).toBe(true)
+  it('exports the public entrypoint', async () => {
+    const mod = await import('../src/index')
+    expect(mod).toBeTruthy()
   })
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/tests/index.test.ts` around lines 3 - 6, Replace the
tautological assertion in the test (the it block under describe('devtools'))
with a real smoke assertion that exercises the package entrypoint: import or
require the package's entrypoint (the module exported by the package under test)
and assert the export exists and has the expected shape (e.g.,
expect(entrypoint).toBeDefined() and if it should be a function assert typeof
entrypoint === 'function' or call it and assert a basic return/behavior). Update
the test body to import the module, check the exported symbol(s) (default export
or named exports) and perform one simple runtime assertion rather than
expect(true).toBe(true).
packages/devtools-a11y/eslint.config.js (1)

7-9: Drop the empty package-level rules block unless overrides are planned.
It currently has no effect and can be removed for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/eslint.config.js` around lines 7 - 9, Remove the empty
top-level "rules" property from the exported config object in eslint.config.js
since it has no effect; locate the object literal that contains rules: {} and
delete that key (or replace it with a clear comment if you intend to add
overrides later) so the package-level config is cleaner and only contains
meaningful settings.
packages/devtools/src/tabs/plugin-registry.ts (1)

224-224: Framework metadata on Line 224 is too narrow for this package.

This entry marks the plugin as react, but this PR adds Solid support too. Consider setting framework to other (or extending metadata model) so marketplace filtering doesn’t hide it from non-React users.

Suggested metadata adjustment
-    framework: 'react',
+    framework: 'other',
-    tags: ['TanStack', 'a11y'],
+    tags: ['TanStack', 'a11y', 'react', 'solid'],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools/src/tabs/plugin-registry.ts` at line 224, The plugin
metadata sets framework: 'react' in the registry entry (the object literal that
includes the framework property), which is too narrow now that Solid support is
added; update that metadata by changing framework to a more generic value such
as 'other' (or add a neutral value like 'multiple' or expand the metadata model
to accept an array like frameworks: ['react','solid']) so marketplace filtering
won't exclude non-React users; locate the object containing framework: 'react'
in plugin-registry.ts (the registry entry for this plugin) and replace the
single-framework value with a generic or multi-framework representation and
update any consuming code that expects a string if necessary.
examples/react/a11y-devtools/src/App.tsx (1)

144-164: Modal accessibility implementation could be enhanced.

While this is a demo app, the modal in the "Accessible Content" comparison section could demonstrate additional best practices:

  • Add aria-modal="true" to indicate it's a modal dialog
  • Consider demonstrating focus management (focus trap, return focus on close)

Since this is an example app showcasing accessibility, demonstrating a fully accessible modal would provide a better reference for users.

♿ Suggested enhancement
       {showModal && (
         <div
           role="dialog"
+          aria-modal="true"
           aria-labelledby="modal-title"
           style={{
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/react/a11y-devtools/src/App.tsx` around lines 144 - 164, Update the
modal implemented where showModal is used (the div with role="dialog" and id
"modal-title") to include aria-modal="true" and implement simple focus
management: when opening (showModal becomes true) save the element that
triggered the modal, move focus to the dialog or its first focusable child, trap
Tab/Shift+Tab inside the dialog, handle Escape to call setShowModal(false), and
on close restore focus to the saved trigger; ensure the existing Close button
still calls setShowModal(false) and that focus restoration happens after
closing.
packages/devtools-a11y/src/react/A11yDevtools.ts (1)

7-7: Consider adding JSDoc for the exported interface.

A11yDevtoolsReactInit is a public API type. Adding documentation would help users understand its purpose and how to extend it. As per coding guidelines, JSDoc should read like documentation as it gets converted to markdown docs for the website.

📝 Suggested documentation
-export interface A11yDevtoolsReactInit extends DevtoolsPanelProps {}
+/**
+ * Initialization options for the A11y DevTools React panel.
+ * Extends the base DevTools panel properties.
+ */
+export interface A11yDevtoolsReactInit extends DevtoolsPanelProps {}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/react/A11yDevtools.ts` at line 7, Add a JSDoc
comment above the exported interface A11yDevtoolsReactInit (which extends
DevtoolsPanelProps) describing its purpose as the React initialization props for
the A11y Devtools panel, what properties consumers can expect/extend, and any
usage notes or examples; keep the comment concise and formatted for doc
generation (summary sentence, longer description, and any `@remarks` or `@example`
if relevant) so the public API is documented for the website.
examples/solid/a11y-devtools/src/app.tsx (1)

124-134: Modal accessibility could be enhanced.

Same as the React example, consider adding aria-modal="true" for a more complete accessible modal demonstration.

♿ Suggested enhancement
       {showModal() && (
         <div
           role="dialog"
+          aria-modal="true"
           aria-labelledby="modal-title"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/solid/a11y-devtools/src/app.tsx` around lines 124 - 134, The modal
div rendered when showModal() is true currently has role="dialog" and
aria-labelledby="modal-title" but lacks aria-modal; update the dialog element
(the div controlled by showModal() / closed via setShowModal(false)) to include
aria-modal="true" (keeping role="dialog" and aria-labelledby="modal-title") so
screen readers treat it as a modal.
examples/solid/a11y-devtools/tsconfig.json (1)

1-24: Consider adding path alias for consistency with the React example (optional).

The React example's tsconfig.json includes a paths configuration for @tanstack/devtools-a11y/* for explicit source mapping during development, while this Solid example relies on pnpm workspace resolution. Both approaches work correctly with workspace:* dependencies and the current configuration, but adding the path alias would improve consistency across examples and provide better IDE support.

Path alias configuration (optional improvement)
{
  "compilerOptions": {
    ...
    "paths": {
      "@tanstack/devtools-a11y/*": ["../../../packages/devtools-a11y/src/*"]
    }
  }
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/solid/a11y-devtools/tsconfig.json` around lines 1 - 24, Add an
optional TypeScript path alias under compilerOptions to match the React example
and improve IDE source mapping: add a "paths" entry mapping
"@tanstack/devtools-a11y/*" to the local source (e.g.
"../../../packages/devtools-a11y/src/*") alongside existing compilerOptions keys
so editors resolve imports consistently with the workspace dependency; update
the tsconfig.json's compilerOptions to include "paths" with that alias.
packages/devtools-a11y/src/core/components/IssueList.tsx (1)

83-85: Minor: Redundant fallback value.

The || 0 fallback is unnecessary since Array.length always returns a non-negative number.

Suggested fix
             const issuesForImpact = () =>
               ally.filteredIssues().filter((issue) => issue.impact === impact)
-            const count = () => issuesForImpact().length || 0
+            const count = () => issuesForImpact().length
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/components/IssueList.tsx` around lines 83 -
85, The count function currently returns issuesForImpact().length || 0 which is
redundant because Array.length is always a non-negative number; update the count
function to return issuesForImpact().length (remove the "|| 0" fallback) —
locate the definitions of issuesForImpact and count in IssueList.tsx and
simplify count to directly return the length.
packages/devtools-a11y/src/core/components/Settings.tsx (1)

24-25: Consider whether availableRules needs to be a memo.

Since getAvailableRules() has no reactive dependencies, wrapping it in createMemo provides no reactivity benefits. A simple variable assignment would suffice unless you anticipate adding reactive dependencies later.

Alternative (if no future reactive dependencies expected)
   const disabledRulesSet = createMemo(() => new Set(config.disabledRules))
-  const availableRules = createMemo(() => getAvailableRules())
+  const availableRules = () => getAvailableRules()

Note: Keep as createMemo if getAvailableRules() is expensive and you want to cache the result.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/components/Settings.tsx` around lines 24 -
25, availableRules is wrapped in createMemo but getAvailableRules() has no
reactive inputs, so remove the unnecessary memoization: replace the createMemo
call for availableRules with a plain assignment (const availableRules =
getAvailableRules()) unless you intentionally want to cache an expensive result;
update the symbol availableRules and the call site that references it if needed,
keeping createMemo only for disabledRulesSet which depends on config.
packages/devtools-a11y/src/core/utils/custom-audit.utils.ts (2)

436-440: Stale comment can be removed.

The comment on lines 438-439 about "adding a parameter in the function signature" for threshold is outdated—the threshold parameter already exists in the function signature (line 434).

Proposed fix
 function checkMouseOnlyEvents(
   context: Document | Element = document,
   threshold: SeverityThreshold = 'serious',
 ): Array<A11yIssue> {
   const issues: Array<A11yIssue> = []
   const timestamp = Date.now()
-  // default threshold will be provided by runCustomRules
-  // We'll accept threshold by adding a parameter in the function signature

   // Build selector for elements with mouse events
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/custom-audit.utils.ts` around lines 436
- 440, Remove the stale comment lines that say "// default threshold will be
provided by runCustomRules" and "// We'll accept threshold by adding a parameter
in the function signature" because the threshold parameter already exists;
locate the block where const issues: Array<A11yIssue> and const timestamp =
Date.now() are declared (immediately after the function signature that contains
threshold) and delete those two comment lines so the code reflects current
reality.

346-348: Performance consideration: querySelectorAll('*') can be expensive.

Querying all elements in the document could be slow on pages with large DOMs. Consider documenting this trade-off or potentially offering a way to scope the audit to specific containers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/custom-audit.utils.ts` around lines 346
- 348, The use of context.querySelectorAll('*') (assigned to allElements) can be
very costly on large DOMs; update the relevant audit function to accept an
optional container/root parameter or selector (e.g., rootElement or
rootSelector) and use rootElement.querySelectorAll('*') instead of querying the
entire context by default, or document the performance trade-off in the function
comment and provide a scoped alternative method (e.g., queryWithin(container) or
an options object) so callers can limit the audit to a subtree when needed.
packages/devtools-a11y/src/core/utils/ally-audit.utils.ts (2)

1-1: Filename typo: "ally" should be "a11y".

The filename ally-audit.utils.ts appears inconsistent with the package naming convention (devtools-a11y). Consider renaming to a11y-audit.utils.ts for consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/ally-audit.utils.ts` at line 1, The
file is misnamed "ally-audit.utils.ts" but the package uses "a11y" naming;
rename the file to "a11y-audit.utils.ts" and update all imports/usages that
reference "ally-audit.utils.ts" to the new filename (search for import paths
referencing ally-audit.utils or the module that exports from it), ensuring any
tooling/config (exports index, barrel files, tests) is updated to the new name
so imports like import ... from '.../a11y-audit.utils' resolve correctly.

21-29: Duplicate constant: IMPACT_SEVERITY is duplicated as SEVERITY_ORDER in ui.utils.ts.

Both files define identical severity-to-number mappings. This creates a maintenance burden if the values ever need to change.

Consider exporting SEVERITY_ORDER from one location and importing it in the other.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/ally-audit.utils.ts` around lines 21 -
29, IMPACT_SEVERITY duplicates SEVERITY_ORDER; remove the duplication by
centralizing the mapping: pick one canonical export (e.g., export const
SEVERITY_ORDER from packages/devtools-a11y/src/core/utils/ally-audit.utils.ts or
vice‑versa), replace the other constant with an import of that exported symbol,
and update any references to use the chosen name (IMPACT_SEVERITY or
SEVERITY_ORDER) so only a single source of truth exists for the
severity-to-number mapping.
packages/devtools-a11y/src/core/utils/ui.utils.ts (3)

478-482: Static analysis: forEach callback return value.

The static analysis tool flags that el.remove() returns undefined from the forEach callback. While functionally correct, using a for...of loop is cleaner and avoids the linter warning.

🔧 Suggested fix
   // Remove tooltips and clear tracking
   const tooltips = document.querySelectorAll(`.${TOOLTIP_CLASS}`)
-  tooltips.forEach((el) => el.remove())
+  for (const el of tooltips) {
+    el.remove()
+  }
   activeTooltips.clear()
   stopScrollListener()
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/ui.utils.ts` around lines 478 - 482,
Replace the NodeList.forEach usage that removes tooltip elements with an
explicit for...of loop to avoid returning undefined from the forEach callback:
iterate over the NodeList returned by
document.querySelectorAll(`.${TOOLTIP_CLASS}`) using for (const el of tooltips)
and call el.remove() inside the loop, leaving the subsequent
activeTooltips.clear() and stopScrollListener() calls unchanged; this targets
the tooltip removal block using TOOLTIP_CLASS, activeTooltips, and
stopScrollListener.

29-31: Module-level mutable state may cause issues in SSR or multi-instance scenarios.

The activeTooltips Map and scrollHandler are module-level singletons. In SSR environments, accessing document or window will throw. Consider guarding DOM access or documenting that these utilities are client-only.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/ui.utils.ts` around lines 29 - 31, The
module exposes module-level mutable singletons activeTooltips and scrollHandler
which cause SSR/multi-instance problems by touching DOM globals; change to
lazy/client-only initialization: remove direct module-level DOM-dependent
initialization and instead create/get these via a function (e.g.,
ensureTooltipState or getTooltipManager) that checks typeof window/document
before accessing DOM, or throw/return no-op in SSR; update usages of
activeTooltips and scrollHandler to call that initializer so multiple instances
or server runs get isolated state and no direct document/window access at module
load time.

466-476: Consider consistency: use for...of loop for classList removal.

For consistency with the suggested fix for tooltips and to avoid potential linter warnings, consider using a for...of loop here as well.

🔧 Suggested fix
 export function clearHighlights(): void {
   // Remove highlight classes
   const highlighted = document.querySelectorAll(`.${HIGHLIGHT_CLASS}`)
-  highlighted.forEach((el) => {
+  for (const el of highlighted) {
     el.classList.remove(
       HIGHLIGHT_CLASS,
       `${HIGHLIGHT_CLASS}--critical`,
       `${HIGHLIGHT_CLASS}--serious`,
       `${HIGHLIGHT_CLASS}--moderate`,
       `${HIGHLIGHT_CLASS}--minor`,
     )
-  })
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/ui.utils.ts` around lines 466 - 476,
Replace the NodeList.forEach call with a for...of loop to iterate over
highlighted elements and remove classes; locate the highlighted variable
assigned via document.querySelectorAll(`.${HIGHLIGHT_CLASS}`) and change the
callback that calls el.classList.remove(...) to an explicit for (const el of
highlighted) { el.classList.remove(HIGHLIGHT_CLASS,
`${HIGHLIGHT_CLASS}--critical`, `${HIGHLIGHT_CLASS}--serious`,
`${HIGHLIGHT_CLASS}--moderate`, `${HIGHLIGHT_CLASS}--minor`) } to match the
tooltip fix style and avoid linter warnings.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/plugins/a11y.md`:
- Around line 63-69: Remove the invalid Vue quick-start code that imports
createA11yDevtoolsVuePlugin from '@tanstack/devtools-a11y/vue' (delete the code
block showing that import and plugin array) and update the framework support
statement to list only React and Solid (remove any mention of Vue and Preact);
ensure any remaining examples or import paths reference only the exported
packages (e.g., ./core, ./react, ./solid) and do not reference
'@tanstack/devtools-a11y/vue' or Preact.

In `@examples/react/a11y-devtools/src/index.tsx`:
- Line 11: Remove the stray "§" character after the JSX self-closing tag which
causes a TSX parse error: locate the TanStackDevtools component usage
(TanStackDevtools plugins={[a11yDevtoolsPlugin()]} ) in index.tsx and delete the
trailing "§" so the line ends with the valid JSX self-closing tag />; ensure no
other stray characters remain on that line.

In `@examples/react/basic/src/setup.tsx`:
- Around line 74-77: The DevTools plugins array contains a duplicated
registration of the TanStack Router plugin — locate the object with name:
'TanStack Router' and render: <TanStackRouterDevtoolsPanel router={router} />
(the duplicate entry near the other registration) and remove the redundant
object so the plugin is only registered once; verify only a single
TanStackRouterDevtoolsPanel usage remains in the plugins list.

In `@packages/devtools-a11y/package.json`:
- Around line 92-103: The package.json lists peerDependencies (preact, vue,
solid-js, `@types/react`, react) but only exports subpaths for ./core, ./react,
./solid and peerDependenciesMeta currently only marks react optional; update
peerDependenciesMeta to mark preact, vue, solid-js and `@types/react` as optional
(in addition to react) so unmet peers don’t emit install warnings, or
alternatively add matching exports for preact/vue if those runtimes are intended
to be supported; modify the package.json's "peerDependenciesMeta" section to
include entries for "preact", "vue", "solid-js", and "@types/react" with
"optional": true to align with the exports and intent.

In `@packages/devtools-a11y/src/core/components/Shell.tsx`:
- Around line 22-30: The guard in handleExport is inverted so the export never
runs; change the check from if (allyResult.audit) return to if
(!allyResult.audit) return so the function returns early when there is no audit
data, then proceed to dynamically import('../utils/export-audit.uitls') and call
m.exportAuditResults(allyResult.audit, { format }); keep using the non-null
assertion only removed by fixing the guard to ensure allyResult.audit is present
before calling exportAuditResults.

In `@packages/devtools-a11y/src/core/contexts/allyContext.tsx`:
- Around line 40-42: triggerAllyScan sets allyResult directly to state 'done' so
the intermediate 'scanning' state is never observed; modify triggerAllyScan to
first call setAllyResult({ audit: null, state: 'scanning' }) (or keep previous
audit if desired), then await runAudit(config) and finally call setAllyResult({
audit: <result>, state: 'done' }); update the function that references runAudit
and setAllyResult (triggerAllyScan) so Shell.tsx can detect allyResult.state ===
'scanning' and show the loading UI.

In `@packages/devtools-a11y/src/core/styles/styles.ts`:
- Around line 138-142: Several control states use low-contrast color pairs
(making small 9–13px text illegible) and the doneButton sets its foreground
equal to its background causing the label to disappear; update the style rules
(e.g., toggleOverlayOn, doneButton and the other similar blocks at the
referenced ranges) to use accessible foreground colors that meet WCAG contrast
for small text—either swap to an appropriate high-contrast token (e.g., a
dedicated on-<color> token such as colorTextOnSuccess/colorTextOnPrimary) or
explicitly set a contrasting color (white or dark text as appropriate) and
ensure border-color matches the visible state so the Done label remains readable
in light theme.

In `@packages/devtools-a11y/src/core/utils/config.utils.ts`:
- Around line 8-14: The DEFAULT_CONFIG object is being returned or merged with
persisted data without validation or cloning, which lets malformed localStorage
values (e.g. disabledRules as a string) and shared array references leak between
instances; update the config-reading functions in this file to: (1) deep-clone
DEFAULT_CONFIG before returning or merging so callers never get the shared
arrays, and (2) sanitize/validate each persisted field (ensure disabledRules is
an array of strings, threshold and ruleSet are one of the allowed string enums,
and showOverlays/persistSettings are booleans) and only accept validated values
from localStorage, falling back to the cloned DEFAULT_CONFIG for any invalid
fields. Ensure the sanitized result is a fresh object/arrays so mutations cannot
affect DEFAULT_CONFIG.

In `@packages/devtools-a11y/src/core/utils/export-audit.uitls.ts`:
- Around line 6-9: The public ExportOptions includes switches includePasses and
includeIncomplete that are not used by exportAuditResults and exportToJson
(these only read format/filename); either remove these no-op fields from the
exported ExportOptions type and any public docs, or wire them up so the
exporters actually honor them: update exportToJson and exportAuditResults to
accept the Partial<ExportOptions> and filter the A11yAuditResult entries
according to includePasses/includeIncomplete before serializing (references:
ExportOptions, includePasses, includeIncomplete, exportToJson,
exportAuditResults, and the format/filename handling) so the public API no
longer advertises no-op options.
- Around line 57-74: Create a CSV-escaping helper (e.g., escapeCsv) that
replaces all double-quotes in a field with two double-quotes and wraps the field
in quotes, then use that helper everywhere CSV fields are produced: when pushing
row values inside the loop over result.issues (the array built into rows), and
when serializing headers and rows in the final return (the headers.map and
rows.map calls). Update references to issue.ruleId, issue.impact, issue.message,
issue.helpUrl, issue.wcagTags.join('; '), node.selector, and node.html so each
passes through escapeCsv before joining into CSV lines.

In `@packages/devtools-ui/src/components/button.tsx`:
- Around line 35-36: The button spreads all incoming props onto the native
<button>, causing component-only props (variant, outline, ghost, children) and
class/className handling issues; use splitProps to separate those local props
from the DOM-safe props, compute the final class string from the extracted local
props (preserving consumer class/className alongside classes()), and then spread
only the remaining safe props onto the <button> while still passing the computed
class and children; update usages around splitProps, props, classes(), and the
<button> element accordingly.

In `@packages/devtools-ui/src/styles/use-styles.ts`:
- Around line 65-87: The warning and success variants in use-styles.ts currently
use white text (text.light/text.dark = '#fff') on yellow[500] and green[500]
which fails WCAG AA; update the warning and success style objects (the "warning"
and "success" symbols) so their solid styles use a high-contrast dark text token
(e.g., tokens.colors.gray[900] for text.light and text.dark) or swap to darker
background shades (e.g., yellow[700], green[700]) for bg.light/bg.dark to
achieve >=4.5:1 contrast, and mirror any hover/active/outline colors as needed
to preserve contrast.

In `@packages/devtools-utils/src/solid/class.ts`:
- Around line 52-55: unmount() currently clears this.#isMounting before the
in-flight mount's awaited import resolves, allowing a concurrent mount() to
start and race; change unmount() to only set this.#abortMount = true and return
(do not set this.#isMounting = false) so the in-flight mount() can observe
this.#abortMount and exit cleanly, and then have mount() be responsible for
clearing this.#isMounting = false when it detects the abort and finishes its
cleanup; update the logic in mount() and unmount() accordingly (references:
methods unmount(), mount(), and private flags `#isMounting` and `#abortMount`).

In `@packages/devtools/src/tabs/plugin-registry.ts`:
- Around line 217-220: The registry entry uses the wrong exported symbol: change
the pluginImport.importName value from 'createA11yPlugin' to
'a11yDevtoolsPlugin' so it matches the actual export from the
`@tanstack/devtools-a11y` package; update the entry where pluginImport is defined
(look for pluginImport.importName) and ensure any consumers expecting
createA11yPlugin are updated to reference a11yDevtoolsPlugin instead.

---

Outside diff comments:
In `@packages/solid-devtools/src/core.tsx`:
- Around line 61-105: Update the JSDoc for the exported property `name` (whose
type is now `string | SolidPluginRender`) to reflect that when a function is
provided it receives the same theme props as `render` (not just the mount
element); explicitly document the callback signature (e.g., receives { theme }
or theme props) and show the example forms (string, JSX element, or a callback
that receives theme props) so the generated docs match the public
`SolidPluginRender` type.

---

Minor comments:
In `@docs/config.json`:
- Around line 181-183: The "to" path for the A11y example is misspelled as
"framework/solif/examples/a11y-devtools" causing a broken docs link; update the
"to" value (the JSON entry paired with "label": "A11y") to the correct path
"framework/solid/examples/a11y-devtools" so the link resolves to the Solid
plugin example.

In `@docs/plugins/a11y.md`:
- Line 10: Replace the adjectival phrase "Full Page Scanning" with the
hyphenated "Full-Page Scanning" in the docs text (the occurrence in
docs/plugins/a11y.md), ensuring the compound adjective is consistently
hyphenated wherever the phrase is used as a descriptor.

In `@examples/react/a11y-devtools/src/App.tsx`:
- Around line 66-68: Update the JSX comment above the example to match the
displayed issue: change the comment text in the div block that contains the h3
("6. Click handler on non-interactive element") so the comment reads "Click
handler on non-interactive element" instead of "Missing main landmark"; locate
the comment in the same JSX fragment containing the div and h3 in App (App.tsx)
and replace the mismatched comment text accordingly.

In `@examples/solid/a11y-devtools/.eslintrc.cjs`:
- Around line 4-9: The ESLint config puts extends and rules under the reserved
settings key so ESLint ignores them; update the exported config object named
config by moving extends and rules out of config.settings into the top-level
config (keep config.settings only for plugin shared data or remove it if
unused), e.g., ensure config.extends = [...] and config.rules = {...} and delete
these keys from config.settings so ESLint reads them correctly.

In `@examples/solid/a11y-devtools/src/app.tsx`:
- Around line 63-65: The inline JSX comment above the example div is
inconsistent with its heading: update either the JSX comment or the <h3> text so
they match; locate the div with style="margin-bottom: 16px" that contains the
<h3> and change the comment to "Click handler on non-interactive element" to
match the heading (or alternatively change the <h3> to "Missing main landmark"
if that was intended).

In `@packages/devtools-a11y/src/core/utils/ally-audit.utils.ts`:
- Around line 110-123: The issue: IDs use Date.now() inside the synchronous loop
(and called twice) which can collide; fix by generating a stable timestamp and a
truly unique id per issue. Before pushing, compute a single timestamp variable
(e.g., const timestamp = Date.now()) and reuse it for the timestamp field, and
replace the id generation with a collision-resistant value (e.g., use
crypto.randomUUID() or append a short random/nonce to
`${violation.id}-${i}-${timestamp}`) so each issue created in the same
millisecond is unique; keep other fields (ruleId, message, help, wcagTags,
nodes, meetsThreshold) unchanged.
- Around line 240-245: The current code casts auditContext to Element when
building the axe.ElementContext (in the if block checking allExclusions.length)
which is unsafe if auditContext can be a Document; change the include array
element cast from Element to Node (e.g., use auditContext as Node) so the type
matches axe-core's ElementContext, and keep the exclude mapping as-is
(allExclusions.map((sel) => [sel])) while preserving the final as
axe.ElementContext assertion.

In `@packages/devtools-a11y/tests/config.test.ts`:
- Around line 27-34: The afterEach cleanup currently calls vi.restoreAllMocks()
but does not undo globals stubbed by vi.stubGlobal('localStorage') in
beforeEach; update the afterEach hook to also call vi.unstubAllGlobals() (in
addition to vi.restoreAllMocks()) to ensure the stubbed global localStorage from
localStorageMock is removed between tests, referencing the existing
beforeEach/afterEach and the vi.stubGlobal('localStorage') usage so the global
cleanup is applied correctly.

---

Nitpick comments:
In `@examples/react/a11y-devtools/src/App.tsx`:
- Around line 144-164: Update the modal implemented where showModal is used (the
div with role="dialog" and id "modal-title") to include aria-modal="true" and
implement simple focus management: when opening (showModal becomes true) save
the element that triggered the modal, move focus to the dialog or its first
focusable child, trap Tab/Shift+Tab inside the dialog, handle Escape to call
setShowModal(false), and on close restore focus to the saved trigger; ensure the
existing Close button still calls setShowModal(false) and that focus restoration
happens after closing.

In `@examples/react/start/src/routes/__root.tsx`:
- Line 1: Replace the namespace import "import * as React from 'react'" with a
type-only import and update usages: change to "import type { ReactNode } from
'react'" and replace any "React.ReactNode" annotations with "ReactNode" (e.g.,
in the return/props types or component signatures); this keeps the runtime
bundle free of an unused React import while preserving the type annotation.
- Around line 39-50: The plugins array is being recreated on every RootDocument
render causing unstable references for TanStackDevtools (which uses it in a
useMemo dependency) and triggering unnecessary devtools.setConfig() calls; fix
this by hoisting the plugins declaration to module scope (move the const plugins
= [...] out of the RootDocument component) so the same array reference is
reused, and keep the existing items (TanStackRouterDevtoolsPanel,
RouteNavigationPanel) intact when referenced by TanStackDevtools/Devtools.

In `@examples/solid/a11y-devtools/src/app.tsx`:
- Around line 124-134: The modal div rendered when showModal() is true currently
has role="dialog" and aria-labelledby="modal-title" but lacks aria-modal; update
the dialog element (the div controlled by showModal() / closed via
setShowModal(false)) to include aria-modal="true" (keeping role="dialog" and
aria-labelledby="modal-title") so screen readers treat it as a modal.

In `@examples/solid/a11y-devtools/tsconfig.json`:
- Around line 1-24: Add an optional TypeScript path alias under compilerOptions
to match the React example and improve IDE source mapping: add a "paths" entry
mapping "@tanstack/devtools-a11y/*" to the local source (e.g.
"../../../packages/devtools-a11y/src/*") alongside existing compilerOptions keys
so editors resolve imports consistently with the workspace dependency; update
the tsconfig.json's compilerOptions to include "paths" with that alias.

In `@packages/devtools-a11y/eslint.config.js`:
- Around line 7-9: Remove the empty top-level "rules" property from the exported
config object in eslint.config.js since it has no effect; locate the object
literal that contains rules: {} and delete that key (or replace it with a clear
comment if you intend to add overrides later) so the package-level config is
cleaner and only contains meaningful settings.

In `@packages/devtools-a11y/src/core/components/IssueList.tsx`:
- Around line 83-85: The count function currently returns
issuesForImpact().length || 0 which is redundant because Array.length is always
a non-negative number; update the count function to return
issuesForImpact().length (remove the "|| 0" fallback) — locate the definitions
of issuesForImpact and count in IssueList.tsx and simplify count to directly
return the length.

In `@packages/devtools-a11y/src/core/components/Settings.tsx`:
- Around line 24-25: availableRules is wrapped in createMemo but
getAvailableRules() has no reactive inputs, so remove the unnecessary
memoization: replace the createMemo call for availableRules with a plain
assignment (const availableRules = getAvailableRules()) unless you intentionally
want to cache an expensive result; update the symbol availableRules and the call
site that references it if needed, keeping createMemo only for disabledRulesSet
which depends on config.

In `@packages/devtools-a11y/src/core/utils/ally-audit.utils.ts`:
- Line 1: The file is misnamed "ally-audit.utils.ts" but the package uses "a11y"
naming; rename the file to "a11y-audit.utils.ts" and update all imports/usages
that reference "ally-audit.utils.ts" to the new filename (search for import
paths referencing ally-audit.utils or the module that exports from it), ensuring
any tooling/config (exports index, barrel files, tests) is updated to the new
name so imports like import ... from '.../a11y-audit.utils' resolve correctly.
- Around line 21-29: IMPACT_SEVERITY duplicates SEVERITY_ORDER; remove the
duplication by centralizing the mapping: pick one canonical export (e.g., export
const SEVERITY_ORDER from
packages/devtools-a11y/src/core/utils/ally-audit.utils.ts or vice‑versa),
replace the other constant with an import of that exported symbol, and update
any references to use the chosen name (IMPACT_SEVERITY or SEVERITY_ORDER) so
only a single source of truth exists for the severity-to-number mapping.

In `@packages/devtools-a11y/src/core/utils/custom-audit.utils.ts`:
- Around line 436-440: Remove the stale comment lines that say "// default
threshold will be provided by runCustomRules" and "// We'll accept threshold by
adding a parameter in the function signature" because the threshold parameter
already exists; locate the block where const issues: Array<A11yIssue> and const
timestamp = Date.now() are declared (immediately after the function signature
that contains threshold) and delete those two comment lines so the code reflects
current reality.
- Around line 346-348: The use of context.querySelectorAll('*') (assigned to
allElements) can be very costly on large DOMs; update the relevant audit
function to accept an optional container/root parameter or selector (e.g.,
rootElement or rootSelector) and use rootElement.querySelectorAll('*') instead
of querying the entire context by default, or document the performance trade-off
in the function comment and provide a scoped alternative method (e.g.,
queryWithin(container) or an options object) so callers can limit the audit to a
subtree when needed.

In `@packages/devtools-a11y/src/core/utils/ui.utils.ts`:
- Around line 478-482: Replace the NodeList.forEach usage that removes tooltip
elements with an explicit for...of loop to avoid returning undefined from the
forEach callback: iterate over the NodeList returned by
document.querySelectorAll(`.${TOOLTIP_CLASS}`) using for (const el of tooltips)
and call el.remove() inside the loop, leaving the subsequent
activeTooltips.clear() and stopScrollListener() calls unchanged; this targets
the tooltip removal block using TOOLTIP_CLASS, activeTooltips, and
stopScrollListener.
- Around line 29-31: The module exposes module-level mutable singletons
activeTooltips and scrollHandler which cause SSR/multi-instance problems by
touching DOM globals; change to lazy/client-only initialization: remove direct
module-level DOM-dependent initialization and instead create/get these via a
function (e.g., ensureTooltipState or getTooltipManager) that checks typeof
window/document before accessing DOM, or throw/return no-op in SSR; update
usages of activeTooltips and scrollHandler to call that initializer so multiple
instances or server runs get isolated state and no direct document/window access
at module load time.
- Around line 466-476: Replace the NodeList.forEach call with a for...of loop to
iterate over highlighted elements and remove classes; locate the highlighted
variable assigned via document.querySelectorAll(`.${HIGHLIGHT_CLASS}`) and
change the callback that calls el.classList.remove(...) to an explicit for
(const el of highlighted) { el.classList.remove(HIGHLIGHT_CLASS,
`${HIGHLIGHT_CLASS}--critical`, `${HIGHLIGHT_CLASS}--serious`,
`${HIGHLIGHT_CLASS}--moderate`, `${HIGHLIGHT_CLASS}--minor`) } to match the
tooltip fix style and avoid linter warnings.

In `@packages/devtools-a11y/src/react/A11yDevtools.ts`:
- Line 7: Add a JSDoc comment above the exported interface A11yDevtoolsReactInit
(which extends DevtoolsPanelProps) describing its purpose as the React
initialization props for the A11y Devtools panel, what properties consumers can
expect/extend, and any usage notes or examples; keep the comment concise and
formatted for doc generation (summary sentence, longer description, and any
`@remarks` or `@example` if relevant) so the public API is documented for the
website.

In `@packages/devtools-a11y/tests/index.test.ts`:
- Around line 3-6: Replace the tautological assertion in the test (the it block
under describe('devtools')) with a real smoke assertion that exercises the
package entrypoint: import or require the package's entrypoint (the module
exported by the package under test) and assert the export exists and has the
expected shape (e.g., expect(entrypoint).toBeDefined() and if it should be a
function assert typeof entrypoint === 'function' or call it and assert a basic
return/behavior). Update the test body to import the module, check the exported
symbol(s) (default export or named exports) and perform one simple runtime
assertion rather than expect(true).toBe(true).

In `@packages/devtools/src/devtools.tsx`:
- Around line 194-200: The effect currently sets
document.documentElement.dataset.tanstackDevtoolsTheme but never restores it on
unmount; capture the previous value (e.g. const prev =
document.documentElement.dataset.tanstackDevtoolsTheme) before assigning
theme(), then register a cleanup to restore it (if prev is undefined delete the
dataset key, otherwise set it back) using Solid's onCleanup inside the
createEffect; update the createEffect block that uses theme() and
document.documentElement.dataset.tanstackDevtoolsTheme to perform this
save-assign-restore sequence.

In `@packages/devtools/src/tabs/plugin-registry.ts`:
- Line 224: The plugin metadata sets framework: 'react' in the registry entry
(the object literal that includes the framework property), which is too narrow
now that Solid support is added; update that metadata by changing framework to a
more generic value such as 'other' (or add a neutral value like 'multiple' or
expand the metadata model to accept an array like frameworks: ['react','solid'])
so marketplace filtering won't exclude non-React users; locate the object
containing framework: 'react' in plugin-registry.ts (the registry entry for this
plugin) and replace the single-framework value with a generic or multi-framework
representation and update any consuming code that expects a string if necessary.

Comment on lines +63 to +69
## Quick Start (Vue)

```ts
import { createA11yDevtoolsVuePlugin } from '@tanstack/devtools-a11y/vue'

const plugins = [createA11yDevtoolsVuePlugin()]
```
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Locate package manifest"
PKG_JSON="$(fd '^package\.json$' packages/devtools-a11y -t f | head -n1)"
echo "$PKG_JSON"

echo "2) Inspect manifest lines mentioning exports/vue/preact"
rg -n '"exports"|vue|preact' "$PKG_JSON"

echo "3) Inspect source entrypoints mentioning vue/preact"
rg -n 'vue|preact' packages/devtools-a11y/src

echo "4) Inspect docs mentions"
rg -n 'Vue|Preact|devtools-a11y/vue|devtools-a11y/preact' docs/plugins/a11y.md

Repository: TanStack/devtools

Length of output: 302


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking package.json exports structure ==="
cat -n packages/devtools-a11y/package.json | sed -n '26,100p'

echo ""
echo "=== Searching for vue/preact implementation files in packages/devtools-a11y ==="
fd -t f '\.(ts|tsx|js|jsx)$' packages/devtools-a11y | grep -E '(vue|preact)' || echo "No vue/preact files found"

echo ""
echo "=== Listing all source files in packages/devtools-a11y root ==="
ls -la packages/devtools-a11y/*.ts 2>/dev/null || echo "No .ts files in root"
ls -la packages/devtools-a11y/src/ | head -20

Repository: TanStack/devtools

Length of output: 2976


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Docs lines 121-123 ==="
sed -n '121,123p' docs/plugins/a11y.md

echo ""
echo "=== Full Vue/Preact mentions in docs ==="
rg -B2 -A2 'Vue|Preact' docs/plugins/a11y.md

Repository: TanStack/devtools

Length of output: 553


Remove Vue and Preact quick-start sections; these frameworks are not exported.

The docs present quick-start code for Vue and claim Preact/Vue support, but the package only exports ./core, ./react, and ./solid. The import path @tanstack/devtools-a11y/vue does not exist and will fail. Remove the Vue section (lines 63–69) and update the framework support statement (line 121) to accurately reflect only React and Solid support.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plugins/a11y.md` around lines 63 - 69, Remove the invalid Vue
quick-start code that imports createA11yDevtoolsVuePlugin from
'@tanstack/devtools-a11y/vue' (delete the code block showing that import and
plugin array) and update the framework support statement to list only React and
Solid (remove any mention of Vue and Preact); ensure any remaining examples or
import paths reference only the exported packages (e.g., ./core, ./react,
./solid) and do not reference '@tanstack/devtools-a11y/vue' or Preact.

Comment on lines +22 to +30
const handleExport = (format: 'json' | 'csv') => {
if (allyResult.audit) return
// Keep export logic in runtime via event -> overlay? export is still a direct helper.
// We keep this import local to avoid pulling export code into the runtime module.

void import('../utils/export-audit.uitls').then((m) =>
m.exportAuditResults(allyResult.audit!, { format }),
)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Inverted guard condition prevents export from ever working.

The condition if (allyResult.audit) return exits the function when audit exists, which is the opposite of the intended logic. The export code on line 28 that uses allyResult.audit! will never be reached when there's data to export.

Proposed fix
   const handleExport = (format: 'json' | 'csv') => {
-    if (allyResult.audit) return
+    if (!allyResult.audit) return
     // Keep export logic in runtime via event -> overlay? export is still a direct helper.
     // We keep this import local to avoid pulling export code into the runtime module.

     void import('../utils/export-audit.uitls').then((m) =>
       m.exportAuditResults(allyResult.audit!, { format }),
     )
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleExport = (format: 'json' | 'csv') => {
if (allyResult.audit) return
// Keep export logic in runtime via event -> overlay? export is still a direct helper.
// We keep this import local to avoid pulling export code into the runtime module.
void import('../utils/export-audit.uitls').then((m) =>
m.exportAuditResults(allyResult.audit!, { format }),
)
}
const handleExport = (format: 'json' | 'csv') => {
if (!allyResult.audit) return
// Keep export logic in runtime via event -> overlay? export is still a direct helper.
// We keep this import local to avoid pulling export code into the runtime module.
void import('../utils/export-audit.uitls').then((m) =>
m.exportAuditResults(allyResult.audit!, { format }),
)
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/components/Shell.tsx` around lines 22 - 30,
The guard in handleExport is inverted so the export never runs; change the check
from if (allyResult.audit) return to if (!allyResult.audit) return so the
function returns early when there is no audit data, then proceed to dynamically
import('../utils/export-audit.uitls') and call
m.exportAuditResults(allyResult.audit, { format }); keep using the non-null
assertion only removed by fixing the guard to ensure allyResult.audit is present
before calling exportAuditResults.

Comment on lines +57 to +74
for (const issue of result.issues) {
for (const node of issue.nodes) {
rows.push([
issue.ruleId,
issue.impact,
issue.message.replace(/"/g, '""'),
issue.helpUrl,
issue.wcagTags.join('; '),
node.selector,
node.html.replace(/"/g, '""'),
])
}
}

return [
headers.map((h) => `"${h}"`).join(','),
...rows.map((row) => row.map((cell) => `"${cell}"`).join(',')),
].join('\n')
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Escape every CSV field through one helper.

Only message and html escape quotes today. Real selectors such as input[type="text"] will produce broken CSV because the field is wrapped but not escaped.

🧾 Proposed CSV escaping fix
+const escapeCsvCell = (value: string) => `"${value.replace(/"/g, '""')}"`
+
 export function exportToCsv(
   result: A11yAuditResult,
   _options: Partial<ExportOptions> = {},
 ): string {
@@
       rows.push([
         issue.ruleId,
         issue.impact,
-        issue.message.replace(/"/g, '""'),
+        issue.message,
         issue.helpUrl,
         issue.wcagTags.join('; '),
         node.selector,
-        node.html.replace(/"/g, '""'),
+        node.html,
       ])
     }
   }
 
   return [
-    headers.map((h) => `"${h}"`).join(','),
-    ...rows.map((row) => row.map((cell) => `"${cell}"`).join(',')),
+    headers.map(escapeCsvCell).join(','),
+    ...rows.map((row) => row.map(escapeCsvCell).join(',')),
   ].join('\n')
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for (const issue of result.issues) {
for (const node of issue.nodes) {
rows.push([
issue.ruleId,
issue.impact,
issue.message.replace(/"/g, '""'),
issue.helpUrl,
issue.wcagTags.join('; '),
node.selector,
node.html.replace(/"/g, '""'),
])
}
}
return [
headers.map((h) => `"${h}"`).join(','),
...rows.map((row) => row.map((cell) => `"${cell}"`).join(',')),
].join('\n')
const escapeCsvCell = (value: string) => `"${value.replace(/"/g, '""')}"`
export function exportToCsv(
result: A11yAuditResult,
_options: Partial<ExportOptions> = {},
): string {
const headers = ['Rule ID', 'Impact', 'Message', 'Help URL', 'WCAG Tags', 'Selector', 'HTML']
const rows: string[][] = []
for (const issue of result.issues) {
for (const node of issue.nodes) {
rows.push([
issue.ruleId,
issue.impact,
issue.message,
issue.helpUrl,
issue.wcagTags.join('; '),
node.selector,
node.html,
])
}
}
return [
headers.map(escapeCsvCell).join(','),
...rows.map((row) => row.map(escapeCsvCell).join(',')),
].join('\n')
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/utils/export-audit.uitls.ts` around lines 57
- 74, Create a CSV-escaping helper (e.g., escapeCsv) that replaces all
double-quotes in a field with two double-quotes and wraps the field in quotes,
then use that helper everywhere CSV fields are produced: when pushing row values
inside the loop over result.issues (the array built into rows), and when
serializing headers and rows in the final return (the headers.map and rows.map
calls). Update references to issue.ruleId, issue.impact, issue.message,
issue.helpUrl, issue.wcagTags.join('; '), node.selector, and node.html so each
passes through escapeCsv before joining into CSV lines.

Comment on lines 65 to +87
warning: {
bg: tokens.colors.yellow[500],
hover: tokens.colors.yellow[600],
active: tokens.colors.yellow[700],
text: '#fff',
border: tokens.colors.yellow[500],
bg: { light: tokens.colors.yellow[500], dark: tokens.colors.yellow[500] },
hover: {
light: tokens.colors.yellow[600],
dark: tokens.colors.yellow[600],
},
active: {
light: tokens.colors.yellow[700],
dark: tokens.colors.yellow[700],
},
text: { light: '#fff', dark: '#fff' },
border: {
light: tokens.colors.yellow[500],
dark: tokens.colors.yellow[500],
},
outline: {
light: tokens.colors.yellow[700],
dark: tokens.colors.yellow[300],
},
outlineHover: {
light: tokens.colors.yellow[600],
dark: tokens.colors.yellow[200],
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

WCAG color contrast requirements AA level text

💡 Result:

WCAG AA color-contrast requirements for text (SC 1.4.3)

  • Normal text (and images of text): at least 4.5:1 contrast against the background. [1]
  • Large-scale text (and images of large-scale text): at least 3:1 contrast. [1]
    • “Large-scale” is commonly interpreted as 18pt+ regular or 14pt+ bold. [2]
  • No minimum contrast required for: inactive UI text, purely decorative/incidental text, text not visible to anyone, and text in logos/brand names (logotypes). [1]

(Related AA requirement, not text: SC 1.4.11 requires 3:1 for key UI component visuals and graphical objects against adjacent colors.) [3]

Sources
[1] WCAG 2.2, Success Criterion 1.4.3 “Contrast (Minimum)” https://www.w3.org/TR/wcag/
[2] W3C (WCAG2ICT) explanation of “large” text (18pt / 14pt bold) https://www.w3.org/TR/2023/DNOTE-wcag2ict-20230815/
[3] W3C (WCAG2ICT) text of SC 1.4.11 “Non-text Contrast” (3:1) https://www.w3.org/TR/wcag2ict-22/


🏁 Script executed:

# Find where color tokens are defined
fd -t f -e "ts" -e "js" -e "json" | xargs rg "yellow.*500" -l | head -20

Repository: TanStack/devtools

Length of output: 106


🏁 Script executed:

# Search for token definitions
find . -type f \( -name "*token*" -o -name "*color*" \) | head -20

Repository: TanStack/devtools

Length of output: 2229


🏁 Script executed:

# Look at the use-styles.ts file context to understand the structure
cat -n packages/devtools-ui/src/styles/use-styles.ts | head -120

Repository: TanStack/devtools

Length of output: 5132


🏁 Script executed:

# Read the tokens file to find actual color values
cat packages/devtools-ui/src/styles/tokens.ts | grep -A 200 "yellow" | head -100

Repository: TanStack/devtools

Length of output: 2408


🏁 Script executed:

python3 << 'EOF'
def rgb_to_luminance(hex_color):
    """Convert hex color to relative luminance per WCAG formula"""
    r = int(hex_color[0:2], 16) / 255.0
    g = int(hex_color[2:4], 16) / 255.0
    b = int(hex_color[4:6], 16) / 255.0
    
    # Apply gamma correction
    def linearize(c):
        if c <= 0.03928:
            return c / 12.92
        else:
            return ((c + 0.055) / 1.055) ** 2.4
    
    r = linearize(r)
    g = linearize(g)
    b = linearize(b)
    
    # Calculate relative luminance
    luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
    return luminance

def contrast_ratio(hex1, hex2):
    """Calculate WCAG contrast ratio between two colors"""
    l1 = rgb_to_luminance(hex1)
    l2 = rgb_to_luminance(hex2)
    
    # Ensure L1 is the lighter color
    if l1 < l2:
        l1, l2 = l2, l1
    
    return (l1 + 0.05) / (l2 + 0.05)

# Calculate contrast for warning variant
yellow_500 = "F79009"
white = "FFFFFF"
ratio = contrast_ratio(yellow_500, white)

print(f"Yellow[500] (`#F79009`) luminance: {rgb_to_luminance(yellow_500):.4f}")
print(f"White (`#FFFFFF`) luminance: {rgb_to_luminance(white):.4f}")
print(f"Contrast ratio: {ratio:.2f}:1")
print(f"\nWCAG AA requirement for normal text: 4.5:1")
print(f"WCAG AA requirement for large text: 3:1")
print(f"Status: {'PASS' if ratio >= 4.5 else 'FAIL (normal text)'}")
print(f"Status: {'PASS' if ratio >= 3 else 'FAIL (large text)'}")
EOF

Repository: TanStack/devtools

Length of output: 294


🏁 Script executed:

python3 << 'EOF'
def rgb_to_luminance(hex_color):
    """Convert hex color to relative luminance per WCAG formula"""
    r = int(hex_color[0:2], 16) / 255.0
    g = int(hex_color[2:4], 16) / 255.0
    b = int(hex_color[4:6], 16) / 255.0
    
    def linearize(c):
        if c <= 0.03928:
            return c / 12.92
        else:
            return ((c + 0.055) / 1.055) ** 2.4
    
    r = linearize(r)
    g = linearize(g)
    b = linearize(b)
    
    luminance = 0.2126 * r + 0.7152 * g + 0.0722 * b
    return luminance

def contrast_ratio(hex1, hex2):
    """Calculate WCAG contrast ratio between two colors"""
    l1 = rgb_to_luminance(hex1)
    l2 = rgb_to_luminance(hex2)
    
    if l1 < l2:
        l1, l2 = l2, l1
    
    return (l1 + 0.05) / (l2 + 0.05)

# Color values from tokens
colors = {
    "yellow": "F79009",
    "blue": "0369A1",
    "red": "DC2626",
    "green": "16A34A",
}
white = "FFFFFF"

print("Contrast ratios for white text on colored backgrounds:\n")
for name, color in colors.items():
    ratio = contrast_ratio(color, white)
    status = "✓ PASS" if ratio >= 4.5 else "✗ FAIL"
    print(f"{name.capitalize():8} ({color}): {ratio:.2f}:1  {status}")

print("\nWCAG AA requirement: 4.5:1 for normal text")
EOF

Repository: TanStack/devtools

Length of output: 296


WCAG AA contrast failure for warning and success variants.

The warning variant uses white text (#fff) on yellow[500] (#F79009), yielding a contrast ratio of 2.35:1—well below the WCAG AA minimum of 4.5:1 for normal text. The success variant has a similar issue: white text on green[500] (#16A34A) produces only 3.30:1 contrast, also failing the 4.5:1 threshold.

Update these variants to use dark text (e.g., gray[900]) for their solid styles, or select darker color shades for the backgrounds. The info (blue: 5.93:1) and danger (red: 4.83:1) variants already meet the requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-ui/src/styles/use-styles.ts` around lines 65 - 87, The
warning and success variants in use-styles.ts currently use white text
(text.light/text.dark = '#fff') on yellow[500] and green[500] which fails WCAG
AA; update the warning and success style objects (the "warning" and "success"
symbols) so their solid styles use a high-contrast dark text token (e.g.,
tokens.colors.gray[900] for text.light and text.dark) or swap to darker
background shades (e.g., yellow[700], green[700]) for bg.light/bg.dark to
achieve >=4.5:1 contrast, and mirror any hover/active/outline colors as needed
to preserve contrast.

Comment on lines +217 to +220
pluginImport: {
importName: 'createA11yPlugin',
type: 'function',
},
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Where registry references the import symbol:"
rg -n --type=ts "['\"]@tanstack/devtools-a11y['\"]|importName:\\s*'createA11yPlugin'|importName:\\s*'a11yDevtoolsPlugin'" packages/devtools/src/tabs/plugin-registry.ts

echo
echo "2) Find definitions/usages of createA11yPlugin vs a11yDevtoolsPlugin:"
rg -n --type=ts "\b(createA11yPlugin|a11yDevtoolsPlugin)\b" packages/devtools-a11y/src

echo
echo "Expected: the registry importName should match an exported symbol from package entrypoints."

Repository: TanStack/devtools

Length of output: 1736


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Check package.json for `@tanstack/devtools-a11y`:"
find . -name "package.json" -path "*/devtools-a11y/*" | head -5

echo
echo "2) Find the main entrypoint files for devtools-a11y:"
fd -e ts -e js "index\.(ts|js)$" packages/devtools-a11y/src

echo
echo "3) Search for any mention of createA11yPlugin in the entire devtools-a11y package:"
rg -n "createA11yPlugin" packages/devtools-a11y/

Repository: TanStack/devtools

Length of output: 412


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "1) Check package.json exports field:"
cat packages/devtools-a11y/package.json | jq '.exports' 2>/dev/null || echo "Could not parse exports"

echo
echo "2) Check core/index.ts exports:"
cat -n packages/devtools-a11y/src/core/index.ts | head -30

echo
echo "3) Check what the actual entry points export for react:"
cat -n packages/devtools-a11y/src/react/index.ts

Repository: TanStack/devtools

Length of output: 1946


Incorrect registry import symbol on line 218.

pluginImport.importName is set to createA11yPlugin, but this symbol does not exist in the @tanstack/devtools-a11y package. The actual exported symbol is a11yDevtoolsPlugin, which is exported from both the React and Solid entrypoints (./react/index.ts and ./solid/index.ts). Change importName to 'a11yDevtoolsPlugin' to match the actual export.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools/src/tabs/plugin-registry.ts` around lines 217 - 220, The
registry entry uses the wrong exported symbol: change the
pluginImport.importName value from 'createA11yPlugin' to 'a11yDevtoolsPlugin' so
it matches the actual export from the `@tanstack/devtools-a11y` package; update
the entry where pluginImport is defined (look for pluginImport.importName) and
ensure any consumers expecting createA11yPlugin are updated to reference
a11yDevtoolsPlugin instead.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/devtools-a11y/src/core/core.tsx`:
- Line 5: The exported interface A11yDevtoolsInit is currently empty; either
remove the export to avoid an unnecessary type (delete the A11yDevtoolsInit
declaration and any imports/uses) or make its intent explicit by adding a short
JSDoc comment above the interface (e.g., “Placeholder for future initialization
options”) so maintainers know it’s intentional; update any downstream
imports/usages accordingly (search for A11yDevtoolsInit) and run the typecheck
to ensure no breakages.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 36b39fe9-9b71-47fc-994d-eac667061ea3

📥 Commits

Reviewing files that changed from the base of the PR and between f560f47 and 7dee13e.

📒 Files selected for processing (1)
  • packages/devtools-a11y/src/core/core.tsx

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/devtools-utils/src/solid/class.ts (1)

57-66: ⚠️ Potential issue | 🔴 Critical

Formatting improvements look good, but the race condition concern remains unaddressed.

The blank line additions improve readability by separating the guard check, abort path, and normal unmount path.

However, the race condition previously flagged on lines 58-61 still exists: clearing #isMounting = false in the abort path (line 60) allows a concurrent mount() to start before the in-flight mount observes #abortMount and exits. The second mount() would reset #abortMount = false (line 34), causing the first mount to proceed anyway—potentially double-mounting.

Note: The main devtools package (packages/devtools/src/core.ts:103-106) uses the identical pattern, so any fix should be applied consistently to both.

,

Suggested fix (per prior review)
       if (this.#isMounting) {
         this.#abortMount = true
-        this.#isMounting = false
         return
       }

The in-flight mount() already sets #isMounting = false when it detects #abortMount (line 40-41), so removing the duplicate clear in unmount() keeps the guard intact until the async operation completes.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-utils/src/solid/class.ts` around lines 57 - 66, In
unmount(), when handling the abort path don't clear the in-flight mount guard
(`#isMounting`) — only set `#abortMount` = true and return so the active async
mount() can observe `#abortMount` and set `#isMounting` = false itself; remove the
line that sets this.#isMounting = false from the abort branch in the unmount()
method (references: unmount(), mount(), private fields `#isMounting` and
`#abortMount`, and cleanup via `#dispose/`#isMounted) and apply the identical change
to the same pattern in the main devtools implementation in
packages/devtools/src/core.ts so both locations behave consistently.
packages/devtools-a11y/src/core/core.tsx (1)

5-5: ⚠️ Potential issue | 🟠 Major

Replace empty interface to clear the Biome error.

Line 5 triggers noEmptyInterface; this is currently a lint error and may fail checks.

Proposed fix
-export interface A11yDevtoolsInit {}
+/** Placeholder for future core initialization options. */
+export type A11yDevtoolsInit = Record<string, never>

As per coding guidelines, **/*.{js,ts,jsx,tsx}: JSDoc should read like documentation as it gets converted to markdown docs for the website.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/core.tsx` at line 5, Replace the empty
interface A11yDevtoolsInit with a type alias to satisfy the noEmptyInterface
rule: change the declaration in core.tsx from "export interface A11yDevtoolsInit
{}" to a type alias such as "export type A11yDevtoolsInit = Record<string,
unknown>"; also add a concise JSDoc comment above the type per project
guidelines so it will render correctly in generated docs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/devtools-a11y/src/core/core.tsx`:
- Line 5: Replace the empty interface A11yDevtoolsInit with a type alias to
satisfy the noEmptyInterface rule: change the declaration in core.tsx from
"export interface A11yDevtoolsInit {}" to a type alias such as "export type
A11yDevtoolsInit = Record<string, unknown>"; also add a concise JSDoc comment
above the type per project guidelines so it will render correctly in generated
docs.

In `@packages/devtools-utils/src/solid/class.ts`:
- Around line 57-66: In unmount(), when handling the abort path don't clear the
in-flight mount guard (`#isMounting`) — only set `#abortMount` = true and return so
the active async mount() can observe `#abortMount` and set `#isMounting` = false
itself; remove the line that sets this.#isMounting = false from the abort branch
in the unmount() method (references: unmount(), mount(), private fields
`#isMounting` and `#abortMount`, and cleanup via `#dispose/`#isMounted) and apply the
identical change to the same pattern in the main devtools implementation in
packages/devtools/src/core.ts so both locations behave consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0560196-48ed-4ca1-8b67-60451a59fc92

📥 Commits

Reviewing files that changed from the base of the PR and between 7dee13e and c6f3711.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (6)
  • examples/react/a11y-devtools/package.json
  • examples/solid/a11y-devtools/package.json
  • packages/devtools-a11y/src/core/core.tsx
  • packages/devtools-utils/bin/intent.js
  • packages/devtools-utils/src/solid/class.ts
  • packages/devtools/src/devtools.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/devtools/src/devtools.tsx
  • examples/solid/a11y-devtools/package.json

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
packages/devtools-a11y/src/core/core.tsx (1)

5-5: ⚠️ Potential issue | 🟡 Minor

Resolve the empty public init type (still equivalent to {}).

Line 5 keeps an empty interface, which still trips Biome and leaves the public API ambiguous. This was already flagged earlier and remains unresolved.

Proposed fix
-export interface A11yDevtoolsInit {}
+export type A11yDevtoolsInit = Record<string, never>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/src/core/core.tsx` at line 5, The exported
A11yDevtoolsInit interface is empty and should be made explicit to avoid
ambiguous public API; replace the empty interface declaration (A11yDevtoolsInit)
with a clear type alias such as export type A11yDevtoolsInit = Record<string,
never> (or export type A11yDevtoolsInit = unknown if you intend an opaque init)
and update any usages/imports to the new type alias so linters and consumers see
an intentional, non-empty public contract.
packages/devtools-a11y/package.json (1)

92-112: ⚠️ Potential issue | 🟡 Minor

Mark @types/react as optional in peer metadata.

Line 93 declares @types/react as a peer, but it is missing from peerDependenciesMeta. This can still generate peer warnings for consumers that do not install React types.

🔧 Proposed fix
  "peerDependenciesMeta": {
+   "@types/react": {
+     "optional": true
+   },
    "preact": {
      "optional": true
    },
    "react": {
      "optional": true
    },
In npm package.json, if "@types/react" is listed in peerDependencies but omitted from peerDependenciesMeta.optional, will consumers without "@types/react" receive unmet peer dependency warnings?
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/devtools-a11y/package.json` around lines 92 - 112, Add
"@types/react" to the peerDependenciesMeta.optional section so it is treated as
optional like the other framework peers: update the package.json to include an
entry for "@types/react" under "peerDependenciesMeta" with "optional": true,
ensuring the existing "peerDependencies" list (which currently contains
"@types/react") remains unchanged; this will prevent npm peer warnings for
consumers who don't install the React types.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/devtools-a11y/package.json`:
- Around line 92-112: Add "@types/react" to the peerDependenciesMeta.optional
section so it is treated as optional like the other framework peers: update the
package.json to include an entry for "@types/react" under "peerDependenciesMeta"
with "optional": true, ensuring the existing "peerDependencies" list (which
currently contains "@types/react") remains unchanged; this will prevent npm peer
warnings for consumers who don't install the React types.

In `@packages/devtools-a11y/src/core/core.tsx`:
- Line 5: The exported A11yDevtoolsInit interface is empty and should be made
explicit to avoid ambiguous public API; replace the empty interface declaration
(A11yDevtoolsInit) with a clear type alias such as export type A11yDevtoolsInit
= Record<string, never> (or export type A11yDevtoolsInit = unknown if you intend
an opaque init) and update any usages/imports to the new type alias so linters
and consumers see an intentional, non-empty public contract.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7680d99b-f339-4f1b-9bc6-2aaf00300310

📥 Commits

Reviewing files that changed from the base of the PR and between c6f3711 and a20fe4a.

📒 Files selected for processing (6)
  • examples/react/a11y-devtools/src/index.tsx
  • examples/react/bundling-repro/.wrangler/deploy/config.json
  • knip.json
  • packages/devtools-a11y/package.json
  • packages/devtools-a11y/src/core/contexts/allyContext.tsx
  • packages/devtools-a11y/src/core/core.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • knip.json
  • packages/devtools-a11y/src/core/contexts/allyContext.tsx

@harry-whorlow harry-whorlow merged commit 7c33985 into TanStack:main Mar 13, 2026
4 of 5 checks passed
@github-actions github-actions bot mentioned this pull request Mar 13, 2026
@LadyBluenotes LadyBluenotes deleted the a11y branch March 13, 2026 16:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants